/*
 * Copyright (C) 2009 Leandro Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.metalframework.tracing;

import java.lang.reflect.*;
import java.util.*;

import javax.annotation.*;

import org.aspectj.lang.*;
import org.aspectj.lang.reflect.*;
import org.metalframework.annotation.*;
import org.metalframework.tracing.impl.*;
import org.slf4j.*;

@InfrastructureComponent
public class TraceInterceptor {
	
	private static final int DEFAULT_MESSAGE_BUFFER_INITIAL_SIZE = 70;
	
	private static ThreadLocal<Boolean> firstInvocation = new ThreadLocal<Boolean>() {
		protected Boolean initialValue() {
			return true;
		}
	};
	
	private TraceParser traceParser;

	public TraceInterceptor() {
		traceParser = new TraceParserDefaultImpl();
	}
	
	public Object invoke(ProceedingJoinPoint ppj) throws Throwable {
		boolean isFirstInvocation = firstInvocation.get();
		if (isFirstInvocation) {
			firstInvocation.set(false);
		}
		
		Object target = ppj.getTarget();
		Method method = ((MethodSignature) ppj.getSignature()).getMethod();
		Object[] arguments = ppj.getArgs();
		
		TracingParameters parameters = getTraceParser().parse(method);
		Logger logger = LoggerFactory.getLogger(target.getClass());
		
		if (shouldLog(logger, parameters, method)) {
			logMessage(logger, parameters, target, method, true, arguments);
		}

		try {
			Object result = ppj.proceed();
			
			if (shouldLog(logger, parameters, method) && parameters.getAfter() != null) {
				logMessage(logger, parameters, target, method, false, result, arguments);
			}

			return result;
		} catch (Throwable t) {
			if (logger.isErrorEnabled()) {
				if (parameters != null && parameters.shouldLog(t)) {
					// Only the first invocation in the chain should log the exception
					if (isFirstInvocation) {
						logger.error(getErrorMessage(target, method, arguments), t);
					}
				}
			}
			throw t;
		} finally {
			if (isFirstInvocation) {
				firstInvocation.remove();
			}
		}
	}
	
	protected boolean shouldLog(Logger logger, TracingParameters parameters, Method method) {
		return parameters != null && parameters.shouldLog(method) && isLogLevelEnabled(logger, parameters);
	}

	protected boolean isLogLevelEnabled(Logger logger, TracingParameters parameters) {
		switch (parameters.getLevel()) {
			case TRACE: return logger.isWarnEnabled();
			case DEBUG: return logger.isDebugEnabled();
			case INFO: return logger.isInfoEnabled();
			case WARN: return logger.isWarnEnabled();
			case ERROR: return logger.isErrorEnabled();
			default: throw new IllegalArgumentException("Unknown log level: " + parameters.getLevel());
		}
	}
	
	protected void logMessage(Logger logger, TracingParameters parameters, Object target, Method method, boolean before, Object... arguments) {
		String message = getMessage(parameters, target, method, before, arguments);
		switch (parameters.getLevel()) {
			case TRACE: logger.trace(message); break;
			case DEBUG: logger.debug(message); break;
			case INFO: logger.info(message); break;
			case WARN: logger.warn(message); break;
			case ERROR: logger.error(message); break;
			default: throw new IllegalArgumentException("Unknown log level: " + parameters.getLevel());
		}
	}
	
	protected String getMessage(TracingParameters parameters, Object target, Method method, boolean before, Object... arguments) {
		String message;
		
		if (before && parameters.getBefore() != null) {
			message = String.format(parameters.getLocale(), parameters.getBefore(), arguments);
		} else if (!before && parameters.getAfter() != null) {
			message = String.format(parameters.getLocale(), parameters.getAfter(), arguments);
		} else {
			StringBuilder sb = new StringBuilder(DEFAULT_MESSAGE_BUFFER_INITIAL_SIZE);
			sb.append("Invoking ");
			appendInvocationInformation(sb, target, method, arguments);
			message = sb.toString();
		}
		
		return message;
	}
	
	protected String getErrorMessage(Object target, Method method, Object... arguments) {
		StringBuilder sb = new StringBuilder(DEFAULT_MESSAGE_BUFFER_INITIAL_SIZE);
		sb.append("Error invoking ");
		appendInvocationInformation(sb, target, method, arguments);
		
		return sb.toString();
	}
	
	protected void appendInvocationInformation(StringBuilder sb, Object target, Method method, Object... arguments) {
		sb.append("method [");
		sb.append(method.getName());
		sb.append("] on object [");
		sb.append(target);
		sb.append("] with arguments ");
		sb.append(Arrays.toString(arguments));
	}
	
	public TraceParser getTraceParser() {
		return traceParser;
	}
	
	@Resource
	public void setTraceParser(TraceParser traceParser) {
		this.traceParser = traceParser;
	}
}
